All files / Bandstand/src/app/api/songs/[id]/cues route.ts

0% Statements 0/86
0% Branches 0/1
0% Functions 0/1
0% Lines 0/86

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87                                                                                                                                                                             
export const dynamic = 'force-dynamic'

import {prisma} from '@/lib/db'
import {requireSession} from '@/lib/guard'
import {imageSize, sniffImage} from '@/lib/avatars'
import {myCues, toCue} from '@/lib/cues-server'
import {route} from '@/lib/route'

type Ctx = {params: {id: string}}

// A 1600px-wide screenshot of a few bars is ~100-400 KB
const MAX_IMAGE = 2_000_000
const MAX_TEXT = 5000
const ANCHOR = /^$|^[^#]{1,80}#\d{1,3}$/

async function songInBand(id: string, bandId: string) {
  return prisma.song.findFirst({where: {id, bandId}, select: {id: true}})
}

/** Your cues on this song (nobody else's: they're personal). */
export const GET = route(async (_req: Request, {params}: Ctx) => {
  const {user, band} = await requireSession()
  if (!(await songInBand(params.id, band.id)))
    return new Response('Not Found', {status: 404})
  return Response.json(
    (await myCues(user.id, [params.id])).get(params.id) ?? [],
  )
})

/**
 * Add a cue. Form fields: anchor, kind (TEXT | IMAGE | ABC), text, and for
 * an image the file (already resized in the browser).
 */
export const POST = route(async (req: Request, {params}: Ctx) => {
  const {user, band} = await requireSession()
  if (!(await songInBand(params.id, band.id)))
    return new Response('Not Found', {status: 404})
  const form = await req.formData()
  const anchor = String(form.get('anchor') ?? '')
  const kind = String(form.get('kind') ?? '')
  const text =
    String(form.get('text') ?? '')
      .trim()
      .slice(0, MAX_TEXT) || null
  if (!ANCHOR.test(anchor) || !['TEXT', 'IMAGE', 'ABC'].includes(kind))
    return new Response('Bad Request', {status: 400})

  let image: {
    mime: string
    width: number
    height: number
    data: Buffer
  } | null = null
  if (kind === 'IMAGE') {
    const file = form.get('file')
    if (!(file instanceof Blob))
      return new Response('Bad Request', {status: 400})
    const data = Buffer.from(await file.arrayBuffer())
    if (!data.length || data.length > MAX_IMAGE)
      return new Response('Payload Too Large', {status: 413})
    const mime = sniffImage(data)
    const size = imageSize(data)
    if (!mime || !size)
      return new Response('Unsupported Media Type', {status: 415})
    image = {mime, ...size, data}
  } else if (!text) return new Response('Bad Request', {status: 400})

  const last = await prisma.cue.findFirst({
    where: {userId: user.id, songId: params.id, anchor},
    orderBy: {position: 'desc'},
    select: {position: true},
  })
  const cue = await prisma.cue.create({
    data: {
      userId: user.id,
      songId: params.id,
      anchor,
      kind: kind as 'TEXT' | 'IMAGE' | 'ABC',
      text,
      position: (last?.position ?? -1) + 1,
      ...(image ? {image: {create: image}} : {}),
    },
    include: {image: {select: {width: true, height: true}}},
  })
  return Response.json(toCue(cue), {status: 201})
})